﻿//************************************************************************************************************
// CoNatural.Data.dll
// Author: Roger Torres
//************************************************************************************************************
// You can use, modify, and distribute the source code and executable programs based on the source code.
// This source code is provided "as is" and without warranties as to performance or merchantability. 
// The author and/or distributors of this source code may have made statements about this source code. 
// Any such statements do not constitute warranties and shall not be relied on by the user in deciding 
// whether to use this source code. This source code is provided without any express or implied warranties 
// whatsoever. Because of the diversity of conditions and hardware under which this source code may be used, 
// no warranty of fitness for a particular purpose is offered. The user is advised to test the source code 
// thoroughly before relying on it. The user must assume the entire risk of using the source code.
//************************************************************************************************************

using System;
using System.Collections.Generic;
using System.Data;
using System.Transactions;
using CoNatural.Data.Binders;

namespace CoNatural.Data {
	/// <summary>
	/// Execution context base
	/// </summary>
	public abstract class ExecutionContextBase : IExecutionContext {
		public ICommand Command { get; private set; }
		protected IDbCommand DbCommand { get; set; }
		private ICommandBinder binder;

		#region IExecutionContext Members

		public IConnection Connection { get; private set; }
		public string CommandName { get; protected set; }
		public string Script { get; protected set; }
		public Exception Error { get; set; }
		public object State { get; set; }

		#endregion

		internal protected void HandleError(Exception ex) {
			if (Error == null)
				Error = ex;
		}

		#region IDisposable Members

		public void Dispose() {
			Close();
			Connection = null;
			Error = null;
			State = null;
		}

		#endregion

		/// <summary>
		/// Constructor
		/// </summary>
		protected ExecutionContextBase(IConnection connection, ICommand command) {
			Connection = connection;
			Command = command;
			if (command != null) { // batch context doesn't use command
				CommandName = command.GetType().Name;
				binder = connection.DataBinder.CreateBinder(command);
			}
		}
    
		#region Bindings

		/// <summary>
		/// Binds properties mapped to input parameters
		/// </summary>
		protected internal virtual void BindIn() { }

		/// <summary>
		/// Binds properties mapped to output parameters
		/// </summary>
		protected internal virtual void BindOut() { }

		#endregion Bindings

		#region DbCommand Management

		/// <summary>
		/// Opens command for execution
		/// </summary>
		internal protected virtual void OpenCommand() {
			Script = Command.GetScript() ?? Connection.DataBinder.ScriptProvider.GetScript(Command); 
			BindIn();
		}

		/// <summary>
		/// Closes command
		/// </summary>
		protected virtual void CloseCommand() {
			BindOut();
		}

		/// <summary>
		/// Opens IDbCommand for execution
		/// </summary>
		protected virtual void OpenDbCommand() {
			// create db command and copy input parameter values
			DbCommand = binder.CreateDbCommand(Connection.DataBinder.DbProvider, Command);

			if(Connection.ExecutionMode != CommandType.StoredProcedure || Command is Script) { // scripts are always executed as text
				DbCommand.CommandType = CommandType.Text;
				DbCommand.CommandText = Script;
			}
			else {
				DbCommand.CommandType = CommandType.StoredProcedure;
				DbCommand.CommandText = binder.DbName;
			}
			DbCommand.CommandTimeout = Connection.TimeOut;

			OpenDbConnection();
		}

		/// <summary>
		/// Closes IDbCommand
		/// </summary>
		protected virtual void CloseDbCommand() {
			if (DbCommand != null) {
				binder.CopyOutput(DbCommand, Command);
				CloseDbConnection();
			}
		}

		/// <summary>
		/// Opens context for execution
		/// </summary>
		protected internal virtual void Open() {
			OpenCommand();

         if(Connection is ConnectionBase)
			   ((ConnectionBase)Connection).RaiseBeforeExecuteEvent(this);

			OpenDbCommand();
		}

		/// <summary>
		/// Closes execution context
		/// </summary>
		public void Close() {
			try {
				CloseDbCommand();
			}
			catch (Exception ex) {
				HandleError(ex);
			}
			DbCommand = null;

			try {
				CloseCommand();
			}
			catch (Exception ex) {
				HandleError(ex);
			}

         if(Connection is ConnectionBase)
			   ((ConnectionBase)Connection).RaiseAfterExecuteEvent(this);
		}

		#endregion DbCommand Management

		#region DbConnection Management

		// share connections when in transaction scope (by thread) to keep lightweight transactions
		[ThreadStatic]
		private static IDictionary<IConnection, SharedConnection> shared_connections;
		private bool is_shared_connection = false;

		protected void OpenDbConnection() {
			// return a new connection when not in transaction scope 
			if (Transaction.Current == null) {
				var connection = Connection.DataBinder.DbProvider.CreateConnection();
				connection.ConnectionString = Connection.ConnectionString;
				connection.Open();
				DbCommand.Connection = connection;
			}
			else {
				// use shared connection
				if(shared_connections == null)
					shared_connections = new Dictionary<IConnection, SharedConnection>();

				SharedConnection sc;
				if (!shared_connections.TryGetValue(Connection, out sc)) {
					sc = new SharedConnection(Connection);
					shared_connections.Add(Connection, sc);
				}
				DbCommand.Connection = sc.Open();
				is_shared_connection = true;
			}
		}

		protected void CloseDbConnection() {
			// don't close shared connections here
			if (!is_shared_connection) {
				var connection = DbCommand.Connection;
				if (connection != null) {
					if (connection.State == ConnectionState.Open)
						connection.Close();
					connection.Dispose();
					DbCommand.Connection = null;
				}
			}
		}

		#endregion DbConnection Management
	}
}
